package com.gzz.redis.util;

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import redis.clients.jedis.Protocol;
import redis.clients.jedis.util.SafeEncoder;

import javax.annotation.Resource;
import java.util.UUID;

/**
 * 简单的redis锁 实现分布式锁
 */
@Slf4j
public class RedisLockUtil {


        @Resource
        private StringRedisTemplate stringRedisTemplate;

        // 当前线程唯一常量
        private ThreadLocal<String> threadKeyId = ThreadLocal.withInitial(() -> UUID.randomUUID().toString());

        /**
         * redis获取锁,获取不到就会不断重试
         * @param key  锁名
         * @param time  锁的最大有效时间(秒)
         */
        public void lock(String key,int time){
            // 锁名
            String keyName = getLockName(key);
            while(true){
                // 争抢锁
                Boolean flag = setnxAndExpire(keyName, threadKeyId.get(), time);
                // 判断key是否存在
                if(!flag){
                    // 判断是否重入锁
                    if(threadKeyId.get().equals(String.valueOf(stringRedisTemplate.opsForValue().get(keyName)))){
                        break;
                    }
                    // 短暂休眠，nano避免出现活锁
                    try {
                        Thread.sleep(500, 500);
                    } catch (InterruptedException e) {
                        break;
                    }
                }else {
                    break;
                }
            }
        }

        /**
         * 尝试获取锁,立即返回结果
         * @param key
         * @param time
         * @return
         */
        public Boolean tryLock(String key,int time){
            // 锁名
            String keyName = getLockName(key);
            return setnxAndExpire(keyName, threadKeyId.get(), time);
        }

        /**
         * 释放锁
         * @param key
         */
        public void unlock(String key){
            // 锁名
            String keyName = getLockName(key);
            // 释放锁
            if (threadKeyId.get().equals(stringRedisTemplate.opsForValue().get(keyName))) {
                stringRedisTemplate.delete(keyName);
            }
        }

        /**
         * 获取锁的名字
         * @param key
         * @return
         */
        public String getLockName(String key){
            if(null != key){
                return "REDIS_LOCK_"+key;
            }else {
                return "DEFAULT_LOCK";
            }
        }

        /**
         * key不存在时设置value,一般用于争抢锁
         * 把setnx和expire合并,保证原子性,
         * @param key
         * @param value 值
         * @param time 秒
         * @return
         */
        public Boolean setnxAndExpire(String key ,String value,long time){
            /**
             * set key value [EX seconds] [PX milliseconds] [NX|XX]
             * EX seconds：设置失效时长，单位秒
             * PX milliseconds：设置失效时长，单位毫秒
             * NX：key不存在时设置value，成功返回OK，失败返回(nil)
             * XX：key存在时设置value，成功返回OK，失败返回(nil)
             *
             * 示例: set name p7+ ex 100 nx
             * SET操作成功后,返回的是OK,失败返回NIL
             */
            return stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> {
                /**
                 * 给定命令与给定参数一起的“本机”或“原始”执行。 该命令按原样执行，并且尽可能少地执行“解释”，这取决于调用方对参数或结果的任何处理
                 */
                Object obj = connection.execute("set"
                        , SafeEncoder.encode(key)
                        , SafeEncoder.encode(value)
                        , SafeEncoder.encode("EX")
                        , Protocol.toByteArray(time)
                        , SafeEncoder.encode("NX"));
                return obj != null;
            });
        }
    }


